package cn.mvc.util;

import java.io.*;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 压缩工具类，只支持 zip 格式
 * TIPS: 已全部确认可用
 *
 * @author yexi
 */
public class CompressUtils {

    public static void compressToZip(String compressTarget) {
        compressToZip(new File(compressTarget));
    }

    public static void compressToZip(String compressTarget, boolean keepDir) {
        compressToZip(new File(compressTarget), keepDir);
    }

    public static void compressToZip(String compressTarget, boolean keepDir, boolean keepEmpty) {
        compressToZip(new File(compressTarget), keepDir, keepEmpty);
    }

    /**
     * 不保留提供目录本身
     */
    public static void compressToZip(File file) {
        compressToZip(file, false);
    }

    /**
     * 保留空目录
     */
    public static void compressToZip(File file, boolean keepDir) {
        compressToZip(file, keepDir, true);
    }

    /**
     * 压缩文件或目录为压缩包，压缩包输出路径为 指定的输出目录
     *
     * @param outputPath 输出目录
     */
    public static void compressToZip(String outputPath, File file, boolean keepDir, boolean keepEmpty) {
        checkPath(outputPath);
        compressToZip(file, keepDir, keepEmpty, outputPath + File.separator + file.getName() + ".zip");
    }

    /**
     * 压缩文件或目录为压缩包，压缩包输出路径为 指定文件或目录所处的目录
     */
    public static void compressToZip(File file, boolean keepDir, boolean keepEmpty) {
        compressToZip(file, keepDir, keepEmpty, file.getPath() + ".zip");
    }

    /**
     * 压缩文件或目录为压缩包
     *
     * @param outputFileName 输出文件
     * @param file           压缩目标
     * @param keepDir        如果压缩目标是目录，是否保留目录自身
     * @param keepEmpty      如果压缩目标是目录，是否保留目录中的空目录(如果 keepDir 为 true 时根目录同时是空目录，则以 keepEmpty 为更高优先级)
     */
    private static void compressToZip(File file, boolean keepDir, boolean keepEmpty, String outputFileName) {
        try {
            if (file.isDirectory()) {
                try (ZipOutputStream zipOutput = new ZipOutputStream(new FileOutputStream(outputFileName))) {
                    // 递归压缩文件夹，parentDirPath参数传""则压缩包就不会有当前文件夹；传file.getName()则有当前文件夹;
                    compressDirectory(zipOutput, file, keepDir ? file.getName() : "", keepEmpty, true);
                    zipOutput.closeEntry();
                }
            } else {
                compressOneFile(file);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 将多个文件压缩至指定目录
     *
     * @param srcFiles        多个文件或目录
     * @param outputPath      输出目录
     * @param compressZipName 压缩文件名
     */
    public static void compressMultipleFile(List<File> srcFiles, String outputPath, String compressZipName) {
        compressMultipleFile(srcFiles, outputPath, compressZipName, true);
    }


    public static void compressMultipleFile(List<File> srcFiles, ZipOutputStream zipOutputStream, boolean keepDirStructure) throws IOException {
        if (keepDirStructure) {
            // 遍历源文件数组
            for (int i = 0; i < srcFiles.size(); i++) {
                File srcFile = srcFiles.get(i);
                if (srcFile.isFile()) {
                    ZipEntry zipEntry = new ZipEntry(srcFile.getName());
                    zipOutputStream.putNextEntry(zipEntry);
                    zipOutputStream.write(Files.readAllBytes(srcFile.toPath()));
                } else {
                    compressDirectory(zipOutputStream, srcFile, srcFile.getName(), true, false);
                }

            }
        } else {
            // 所有文件名，从而可以判断文件名是否重复
            Set<String> allFileNames = new HashSet<>();
            for (int i = 0; i < srcFiles.size(); i++) {
                File srcFile = srcFiles.get(i);
                compressNoStructure(allFileNames, srcFile, zipOutputStream);
            }
        }
    }
    /**
     * @param srcFiles         多个源
     * @param outputPath       输出目录
     * @param compressZipName  压缩后的zip文件名。不用加后缀。
     * @param keepDirStructure 是否保留目录结构(不保留的话所有文件都塞到压缩包根目录下，若出现文件同名现象则以后加(1)这样的形式加进去)
     */
    public static void compressMultipleFile(List<File> srcFiles, String outputPath, String compressZipName, boolean keepDirStructure) {
        // 如果源文件都不存在的话就不用压缩了
        srcFiles = srcFiles.stream().filter(File::exists).collect(Collectors.toList());
        if (srcFiles.size() == 0) {
            return;
        }
        checkPath(outputPath);
        String outputFileName = outputPath + File.separator + compressZipName + ".zip";
        try (FileOutputStream fileOutputStream = new FileOutputStream(outputFileName);
             ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream)
        ) {
            compressMultipleFile(srcFiles, zipOutputStream, keepDirStructure);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void compressNoStructure(Set<String> allFileNames, File srcFile, ZipOutputStream zos) throws IOException {
        // 文件原名
        String originalName = srcFile.getName();
        if (srcFile.isFile()) {
            int seq = 0;
            String newName = originalName;
            // 解决重名问题
            while (allFileNames.contains(newName)) {
                seq++;
                int suffix = originalName.lastIndexOf(".");
                if (suffix != -1) {
                    newName = originalName.substring(0, suffix) + "(" + seq + ")" + originalName.substring(suffix);
                } else {
                    newName = originalName + "(" + seq + ")";
                }
            }
            allFileNames.add(newName);
            zip(zos, srcFile, newName, false);
        } else {
            for (File file : srcFile.listFiles()) {
                compressNoStructure(allFileNames, file, zos);
            }
        }
    }


    /**
     * 递压缩目录的递归方法，从目录下通过递归调用此方法直至找到文件进行压缩
     *
     * @param dir           必然是目录
     * @param parentDirPath 上级目录路径，如果是""则代表压缩后将不包含此目录
     * @param keepEmpty     是否保留空目录
     * @param isRoot        是否是根目录(用于解决保留根目录为空目录时产生的冲突判断)
     */
    private static void compressDirectory(ZipOutputStream zipOutput, File dir, String parentDirPath,
                                          boolean keepEmpty, boolean isRoot) {
        // 列出目录下的所有文件
        File[] listFiles = dir.listFiles();
        // 不判空，保留报错机制(传成文件或发生IO异常时会返回null)
        // 如果是空目录 且 开启了保留空目录机制
        if (keepEmpty && listFiles.length == 0) {
            // 如果这个空目录不是根目录 或 有上级目录路径(根目录时且需要保留目录时需要)
            if (!isRoot || !"".equals(parentDirPath)) {
                zip(zipOutput, dir, parentDirPath + File.separator, true);
            }
        } else {
            for (File file : listFiles) {
                if (file.isDirectory()) {
                    // 若为""，则只要传 当前目录 作为子内容的上级目录，否则的话就是 上级目录/当前目录 作为子内容的上级目录
                    if ("".equals(parentDirPath)) {
                        compressDirectory(zipOutput, file, file.getName(), keepEmpty, false);
                    } else {
                        compressDirectory(zipOutput, file, parentDirPath + File.separator + file.getName(), keepEmpty, false);
                    }
                } else {
                    if ("".equals(parentDirPath)) {
                        zip(zipOutput, file, file.getName(), false);
                    } else {
                        zip(zipOutput, file, parentDirPath + File.separator + file.getName(), false);
                    }
                }
            }
        }
    }

    /**
     * 压缩的具体操作
     *
     * @param file         压缩条目目标
     * @param zipEntryName 实际的压缩包内文件
     * @param isEmpty      压缩目标是否是空目录
     */
    private static void zip(ZipOutputStream zipOutput, File file, String zipEntryName, boolean isEmpty) {
        try {
            ZipEntry zipEntry = new ZipEntry(zipEntryName);
            zipOutput.putNextEntry(zipEntry);
            // 只要传的不是空目录
            if (!isEmpty) {
                zipOutput.write(Files.readAllBytes(file.toPath()));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 将单个文件压缩成压缩包
     */
    public static void compressOneFile(ZipOutputStream zipOutput, File file) {
        try {
            zipOutput.putNextEntry(new ZipEntry(file.getName()));
            zipOutput.write(Files.readAllBytes(file.toPath()));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            close(zipOutput);
        }
    }

    /**
     * 将单个文件压缩成压缩包，并输出至指定的目录之中
     */
    public static void compressOneFile(String outputPath, String compressZipName, File file) throws FileNotFoundException {
        checkPath(outputPath);
        compressOneFile(new ZipOutputStream(new FileOutputStream(outputPath + File.separator + compressZipName + ".zip")), file);
    }

    /**
     * 将单个文件压缩成压缩包，并输出至文件本身所处的目录当中
     */
    public static void compressOneFile(File file) throws FileNotFoundException {
        compressOneFile(file.getParentFile().getPath(), file.getName().substring(0, file.getName().lastIndexOf(".")), file);
    }

    /**
     * @param outputPath 输出目录
     */
    private static void checkPath(String outputPath) {
        File outputPathFile = new File(outputPath);
        if (!outputPathFile.exists()) {
            outputPathFile.mkdirs();
        }
    }

    private static void close(AutoCloseable... targets) {
        for (AutoCloseable target : targets) {
            try {
                if (target != null) {
                    target.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}
